﻿
using ICSharpCode.AvalonEdit.Document;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;


namespace BoilenEditor.Primitives {

    internal static class SearchBehavior {

        public static void Search( SearchWindow window, BindableTextEditor editor, SearchData search, bool? findNext ) {
            bool isReplace = !findNext.HasValue && !search.IsFindOperation;
            bool next = findNext ?? search.Next;
            Searcher searcher = GetSearcher( search.Find, next, search.UseRegex, search.CaseSensitive );

            bool hasMatched =
                isReplace && search.All
                    ? ReplaceAll( editor, search, searcher )
                    : FindReplace( editor, search, searcher, next, isReplace );

            // If no match was found, inform user.
            if( !hasMatched ) {
                Window owner = window.IsVisible ? window : window.Owner;
                MessageBox.Show( owner, "The specified text was not found.", "Search", MessageBoxButton.OK, MessageBoxImage.Information );
            }
        }


        private static bool ReplaceAll( BindableTextEditor editor, SearchData search, Searcher searcher ) {
            // Get all matches on all lines.
            IEnumerable<DocumentLine> lines = Enumerable.Range( 0, editor.LineCount ).Select( i => editor.Document.Lines[i] );
            var lineMatches = GetMatches( editor, searcher, lines, true, null )
                .GroupBy( match => match.LineIndex )
                .OrderBy( group => group.Key )
                .ToArray( );
            bool hasMatched = lineMatches.Any( );

            if( hasMatched ) {
                // Make all replacements in one change to the document's text.
                int matchIndex = 0;
                string[] fullText = new string[editor.LineCount];
                for( int i = 0; i < fullText.Length; ++i ) {
                    string line = editor.GetLineText( i );
                    if( matchIndex < lineMatches.Length && lineMatches[matchIndex].Key == i ) {
                        SearchMatch[] matches = lineMatches[matchIndex].OrderBy( match => -match.Start ).ToArray( );
                        ++matchIndex;

                        StringBuilder lineText = new StringBuilder( line );
                        foreach( SearchMatch match in matches ) {
                            lineText.Remove( match.Start, match.Length );
                            lineText.Insert( match.Start, search.Replace );
                        }

                        line = lineText.ToString( );
                    }

                    fullText[i] = line;
                }

                string text = string.Join( Environment.NewLine, fullText );
                editor.Document.Text = text;
            }

            return hasMatched;
        }

        private static bool FindReplace( BindableTextEditor editor, SearchData search, Searcher searcher, bool next, bool isReplace ) {
            int currentLineNumber = editor.GetLineIndexFromCharacterIndex( editor.SelectionStart );
            DocumentLine currentLine = editor.Document.Lines[currentLineNumber];
            bool hasMatched = false;

            // If replacing, replace current selected text, if it is a match.
            LineMatch replaceLineMatch;
            if( isReplace && searcher( editor.SelectedText, 0, out replaceLineMatch ) ) {
                hasMatched = true;
                editor.SelectedText = search.Replace;   // TODO: support regex replace with captures
                editor.SelectionStart += editor.SelectionLength;
                editor.SelectionLength = 0;
            }

            // Find and select next match.
            IEnumerable<DocumentLine> lines = GetLines( editor, currentLineNumber, next, search.Wrap );
            IEnumerable<SearchMatch> matches = GetMatches( editor, searcher, lines, currentLine.Offset, next );
            SearchMatch firstMatch = matches.FirstOrDefault( );
            if( firstMatch != null ) {
                var line = firstMatch.Line;
                int matchOffset = line.Offset + firstMatch.Start;
                int matchLength = firstMatch.Length;

                hasMatched = editor.SelectionStart != matchOffset || editor.SelectionLength != matchLength;
                editor.Select( matchOffset, matchLength );
                editor.ScrollTo( line.LineNumber, firstMatch.Start );
            }

            return hasMatched;
        }


        private static IEnumerable<SearchMatch> GetMatches( BindableTextEditor editor, Searcher searcher, IEnumerable<DocumentLine> lines, int currentLineOffset, bool next ) {
            int selectionIndex = editor.SelectionStart + (next ? 0 : editor.SelectionLength - 1);
            int? firstIndex = selectionIndex - currentLineOffset;
            int matchIndexOffset = next ? +1 : -1;

            if( next && editor.SelectionLength == 0 )
                firstIndex -= matchIndexOffset;

            return GetMatches( editor, searcher, lines, next, firstIndex );
        }

        private static IEnumerable<SearchMatch> GetMatches( BindableTextEditor editor, Searcher searcher, IEnumerable<DocumentLine> lines, bool next, int? firstIndex ) {
            int matchIndexOffset = next ? +1 : -1;

            foreach( var line in lines ) {
                string text = editor.Document.GetText( line );
                int matchIndex = firstIndex ?? (next ? -1 : text.Length);

                LineMatch match;
                while( matchIndex + matchIndexOffset >= 0
                    && matchIndex + matchIndexOffset < text.Length
                    && searcher( text, matchIndex + matchIndexOffset, out match ) ) {
                    yield return new SearchMatch( line, match );
                    matchIndex = match.Start;
                }

                firstIndex = null;
            }
        }

        private static Searcher GetSearcher( string find, bool next, bool useRegex, bool caseSensitive ) {
            Searcher searcher;
            if( useRegex ) {
                RegexOptions options = RegexOptions.Singleline;
                if( !caseSensitive )
                    options |= RegexOptions.IgnoreCase;

                Func<IEnumerable<Match>, int, Match> filter;
                if( next )
                    filter = ( ms, i ) => ms.FirstOrDefault( m => m.Index >= i );
                else
                    filter = ( ms, i ) => ms.LastOrDefault( m => m.Index + m.Length <= i );

                searcher = ( string t, int i, out LineMatch lm ) => {
                    var matches = Regex.Matches( t, find, options );
                    Match match = filter( matches.Cast<Match>( ), i ) ?? Match.Empty;
                    IEnumerable<string> captures =
                        match.Groups.Cast<Group>( )
                             .SelectMany( group => group.Captures.Cast<Capture>( ) )
                             .Select( c => c.Value );
                    lm = new LineMatch( match.Value, match.Index, captures );
                    return lm.Success;
                };
            }
            else {
                StringComparison comparison =
                    caseSensitive
                        ? StringComparison.Ordinal
                        : StringComparison.OrdinalIgnoreCase;

                searcher = ( string t, int i, out LineMatch lm ) => {
                    int found = next
                         ? t.IndexOf( find, i, comparison )
                         : t.LastIndexOf( find, i, comparison );
                    lm = new LineMatch( find, found );
                    return lm.Success;
                };
            }

            return searcher;
        }

        private static IEnumerable<DocumentLine> GetLines( BindableTextEditor editor, int currentLine, bool next, bool wrap ) {
            IEnumerable<int> lineIndices;
            if( next ) {
                lineIndices = Enumerable.Range( currentLine, editor.LineCount - currentLine );
                if( wrap && currentLine > 0 ) {
                    IEnumerable<int> wrapIndicies = Enumerable.Range( 0, currentLine );
                    lineIndices = lineIndices.Concat( wrapIndicies ).Concat( new[] { currentLine } );
                }
            }
            else {
                int nextLine = currentLine + 1;
                lineIndices = Enumerable.Range( 0, nextLine ).Reverse( );
                if( wrap && currentLine + 1 < editor.LineCount ) {
                    IEnumerable<int> wrapIndicies = Enumerable.Range( nextLine, editor.LineCount - nextLine ).Reverse( ).Concat( new[] { currentLine } );
                    lineIndices = lineIndices.Concat( wrapIndicies );
                }
            }

            return lineIndices.Select( i => editor.Document.Lines[i] );
        }


        private delegate bool Searcher( string text, int start, out LineMatch match );

        private sealed class LineMatch {
            public readonly string Match;
            public readonly int Start;
            public readonly string[] Captures;

            public int Length {
                get { return this.Match.Length; }
            }

            public bool Success {
                get {
                    return this.Start >= 0
                        && this.Length > 0;
                }
            }

            public LineMatch( string match, int start )
                : this( match, start, new[] { match } ) { }
            public LineMatch( string match, int start, IEnumerable<string> captures ) {
                this.Match = match;
                this.Start = match.Length > 0 ? start : -1;
                this.Captures = captures.ToArray( );
            }

            public override string ToString( ) {
                return this.Success
                     ? this.Match + " (" + this.Start + ")"
                     : "";
            }
        }

        private sealed class SearchMatch {
            public readonly DocumentLine Line;
            public readonly int LineIndex;
            public readonly int Start;
            public readonly int Length;

            public SearchMatch( DocumentLine line, LineMatch match ) {
                this.Line = line;
                this.LineIndex = line.LineNumber - 1;
                this.Start = match.Start;
                this.Length = match.Length;
            }

            public override string ToString( ) {
                return string.Format( "SearchMatch: LineIndex={0}, Start={1}, Length={2}", this.LineIndex, this.Start, this.Length );
            }
        }

    }

}
